typen = [ 'al', # kreuze alle richtigen antworten an (alle)
          'me'] # kreuze mindestens eine richtige an, (mindestens eine)

from google.appengine.ext import db
from transaction.mopay.einstellungen import BEARBEITUNGSZEIT 
import string, random


from db_key import get_key_name
from policy.models import Policy
from const.models import Const
from utils import wann

from statlib import stats # statistische funktionen, mean, stdev etc. sehr nuetzlich

def get_BEARBEITUNGSZEIT():
  c = Const.get_by_key_name('const')
  if c is None:
    return 120.0 # zwei Minuten
  else:
    return c.bearbeitungszeit

def create_interval_list(p):
  """ Bsp:
      p = [0.3,0.1,0.6] --> [(0.0,0.3),(0.3,0.4),(0.4,1.0)]
  """
  ils = []
  for i in range(0,len(p)):
    if i == 0:
      ils.append((0.0,p[i]))
    elif i == len(p)-1: # wegen rundungsfehler 0.99999 an stelle von 1.0 muessen wir den fall gesondert betrachten:
      ils.append((ils[i-1][1],1.0))
    else:
      ils.append((ils[i-1][1],ils[i-1][1]+p[i]))
  return ils


def choose_from_interval_list(interval_list,x, return_interval=False):
  """ Waehlt das Intervall in dem die Zahl x liegt
      und gibt das Intervall zurueck, falls return_interval=True,
      ansonsten den Index des Intervalls in der Intervallliste
      interval_list.
  """
  for interval in interval_list:
    if interval[0] <= x <= interval[1]:
      if return_interval:
        return interval
      else:
        return interval_list.index(interval)

def gewonnen(i,p,a):
  if len(p) == 1: # hat nur ein Element, nicht veraendern
    return p
  new_p = []
  for j in range(0,len(p)):
    if j == i:
      new_p.append(p[i]+a*(1-p[i]))
    else:
      new_p.append(p[j]*(1-a))
  return new_p

# a = 0.01*min(p)*len(p)

def verloren(i,p,a):
  if len(p) == 1: # nur ein Element, nicht veraendern
    return p
  new_p = []
  for j in range(0,len(p)):
    if j == i:
      new_p.append(p[i]*(1-a))
    else:
      new_p.append(p[j] +a*1.0/(len(p)-1)*p[i])
  return new_p


def dict2list(d):
  """ Wandle {'a':1,'b':3} zu [(a,1),(b,3)] um
  """
  return zip(d.keys(),d.values())

def list2dict(l):
  """ Wandle [(a,1),(b,3)] zu {'a':1,'b':3} um
  """
  return dict(l)


def get_policy(akey,ist_richtig):
    key_name = get_key_name(prefix='policy',akey=akey)
    p = Policy.get_by_key_name(key_name)
    if p is None: # d.h. wir haben keine Policy, in diesem Fall ist die Policy einfach auf sich zu verlinken
      p = Policy.get_or_insert(key_name)
      p.akey = akey
      p.gewonnen = akey
      p.verloren = akey
      p.put()
      return akey
    else:
      if ist_richtig:
        return p.gewonnen
      else:
        return p.verloren


class Loesung(db.Model):
  akey = db.StringProperty()
  pdf = db.BlobProperty()
  html_tex = db.TextProperty()
  video_hochgeladen = db.BooleanProperty()  

class QAufgabe(db.Model):
  created = db.DateTimeProperty(auto_now_add=True)
  last_modified = db.DateTimeProperty(auto_now = True)
  richtig_bearbeitet = db.FloatProperty()
  bearbeitet = db.FloatProperty()
  links = db.StringListProperty()
  p = db.StringListProperty()
  bearbeitungs_zeiten = db.ListProperty(float) # die Zeiten in Sekunden, die Besucher gebraucht haben, um die Aufgabe zu bearbeiten
  tmp_sgrad = db.FloatProperty()
  akey = db.StringProperty()
  tipp = db.TextProperty()
  frage = db.TextProperty()           # 'Aufgabe: <br/> Was ergibt <strong>1-3</strong>?'
  antworten = db.StringListProperty()
  loesungen = db.StringListProperty()
  tags = db.StringListProperty()            
  typ = db.StringProperty(choices=set(['al','me']))              
  titel = db.StringProperty()

  _alpha = 0.5 # wiederholungsfaktor beim mehrmaligen posten einer aufgabe
              # wird an mehreren stellen gebraucht, deswegen hier speichern

  _bearbeitungszeit = get_BEARBEITUNGSZEIT() #BEARBEITUNGSZEIT # Bearbeitungszeit in Sekunden, falls nichts anderes vorhanden, z.B. bearbeitungs_zeiten

  def wann(self):
    return wann(self.created)

  def get_bearbeitungszeit(self,besucher_fitness=0.0):
    """ Liefert die Bearbeitungszeit in Sekunden an. Das ist der Countdown, der
        beim Benutzer erscheint. Die Bearbeitungszeit hat folgende Aufgaben:
          (1) Den Besucher in eine wettbewerbsorientierte Stimmung zu versetzen
          (2) Den Besucher laenger auf die Seite zu behalten
          (3) Dem Besucher ein positives Gefuehl zu vermitteln, falls er die Aufgabe
              richtig unter der gegebenen Zeit bearbeitet
        Dazu wird als Bearbeitungszeit folgende Zeit gegeben:
                 Erwartungswert + 3*Standardabweichung (bezogen auf dei bearbeitungs_zeiten)
        3-Sigma-Intervall--> mind. 88,9% aller Besucher liegen dadrin.
        Falls bearbeitungs_zeiten = [], wird standardmaessig 300 Sek. zurueckgegeben.
        besucher_fitness = Anzahl der richtig geloesten Aufgaben / Anzahl der bearbeiteten Aufgaben (aus Besuchersicht)
    """
    bz = self.bearbeitungs_zeiten
    if len(bz)>7: # ab 8 Zeiten macht es sinn den Mittelwert und StdAbw zu betrachten
      # 1-besucher_fitness  --> je besser ein Besucher ist, desto weniger Bearbeitungszeit hat er, aber immer noch genug
      return int(stats.lmean(bz)+(3+5*(1-besucher_fitness)*random.random())*stats.lstdev(bz))
    else:
      BZ = get_BEARBEITUNGSZEIT()
      return int(BZ * (1+((-1)**random.randint(0,1))*random.randint(0,10)*1.0/100.0))
              ## ^-- Standardbearbeitungszeit +- zufallswert von hoechstens 10%

  def get_nakey(self):
    """ Gibt ein akey fuer eine neue Aufgabe an.
    """
    if self.p == []:
      self.p = [str(1.0/len(self.links)) for l in self.links] # sehr verspaetete initialisierung von p
      self.put()
    p_float = [eval(p) for p in self.p]
    pils = create_interval_list(p_float)
    pr_knoten = random.random()
    knoten = choose_from_interval_list(pils,pr_knoten)
    return self.links[knoten]

  def update_p(self,link, ist_richtig = True):
    """ Update von p, link = akey der vorherigen Aufgabe
    """ 
    i = self.links.index(link)
    if self.p == []:
      self.p = [str(1.0/len(self.links)) for l in self.links] # sehr verspaetete initialisierung von p
      self.put()

    p_float = [eval(p) for p in self.p]
    a = 0.1 #*min(p_float)*len(p_float) # numerische Experimente durchgefuehrt, klappt bei 1<= len(p) <= 100
    if ist_richtig:
      new_p = gewonnen(i, p_float, a)
    else:
      new_p = verloren(i, p_float, a)
    self.p = [str(p) for p in new_p]
    self.put()
    return None

  def init_bearbeitet(self):
    self.bearbeitet = 0.0
    self.richtig_bearbeitet = 0.0


  def put_antworten(self, antworten):
    if not isinstance(antworten,dict):
      raise Exception('Antworten nicht vom Typ dict: %s' % antworten)
    else: # {'a':'2+2','b':....} --> keys sind kleinbuchstaben a,b,c,d ..
      keys = antworten.keys()
      keys.sort() # sortieren
      if string.ascii_lowercase.find(''.join(keys)) == 0 :# checken ob wirklich nur kleinbuchstaben in der richtigen reihenfolge von a beginnend
        alist = dict2list(antworten)
        self.antworten = [str(a) for a in alist]
        self.put() # speichern
        return True
      else:
        raise Exception('Falsches Format fuer Antworten : %s ' % keys)

  def put_loesungen(self, loesungen):
    if not isinstance(loesungen,set):
      raise Exception('Loesungen muessen vom Typ set sein: %s ' % loesungen)
    else:
      antworten = self.get_antworten()
      if not loesungen.issubset(set(antworten.keys())) or len(loesungen) == 0:
        raise Exception('loesungen = %s muss eine nichtleere Teilmenge von antworten.keys() = %s sein' % (loesungen, antworten.keys()))
      else:
        self.loesungen = list(loesungen) # set in eine liste umwandeln
        self.put() # speichern
        return True

  def get_antworten(self, format = 'dict'):
    aliste  = [eval(a) for a in self.antworten]
    aliste.sort() # sortieren
    if format == 'list':
      return aliste
    elif format == 'dict':
      return list2dict(aliste)
    else:
      raise Exception('Wrong format: %s ' % format)

  def get_loesungen(self):
    return  set(self.loesungen)


  def gewinn_ws(self):
    """ Gewinn-WS eines Besuchers, der zufaellig die Aufgabe ankreuzt.
        Die Gewinn-WS berechnet sich aus dem theoretischen Wert (am Anfang bevorzugt)
        und der relativen Haeufigket (am Ende bevorzugt) aus der Formel:
        t = theroetische WS
        h = relative Haeufigkeit
        n = self.bearbeitet

                 1.0                        1.0
        g = ------------- * t  + (1 -  --------------  ) * h 
             math.log(n+3)              math.log(n+3)

    """
    import math
    n = self.bearbeitet
    if n > 0:
      h = self.richtig_bearbeitet*1.0 / n
    else:
      h = self.tmp_sgrad

    if self.typ == 'me':
      t = 1.0*(2**len(self.get_loesungen())-1)/2**len(self.get_antworten())
    elif self.typ == 'al':
      t = 1.0 / 2**len(self.get_antworten())
    else:
      raise Exception("Falscher Aufgabentyp %s " % self.typ )
    return 1.0/math.log(n+3)*t + (1-1.0/math.log(n+3))*h

  def get_sgrad(self): #sgrad = Schwierigkeitsgrad
      return 1-1.0*self.gewinn_ws()


  def hinweis(self):
    """ Gibt einen Hinweistext zur Anzahl der anzukreuzenden Antworten an.
        Dieser Text erscheint auf der Aufgabenseite.
    """
    if len(self.get_loesungen())==1:
      return "Kreuze die richtige Antwort an."
    elif len(self.get_loesungen())>1:
      if self.typ == 'al': 
        wieviele = 'alle richtigen Antworten'
      else:
        wieviele = 'mindestens eine richtige Antwort'
      return "Es sind mehrere Antworten richtig. <br/> Kreuze %s an." % wieviele
    else:
      return ""


  def zu_gewinnen(self, anz_bearbeitet):
    return 10.0*self.get_sgrad()*self._alpha**anz_bearbeitet
  
  def zu_verlieren(self, anz_bearbeitet):
    return self.zu_gewinnen(anz_bearbeitet) * 0.5

  def update_bearbeitet(self, meine_antworten, anz_bearbeitet):
    """ anz_bearbeitet = Wie oft hat der Benutzer vorher seine Antworten gepostet?
    """
    self.bearbeitet += self._alpha**anz_bearbeitet
    if self.ist_richtig(meine_antworten):
      self.richtig_bearbeitet += self._alpha**anz_bearbeitet
    self.put() # speichern

  def ist_richtig(self,meine_antworten):
    """ meine_antworten = Liste von Strings z.b. ['a','c']
        gibt True  / False aus, je nachdem ob es richtig oder falsch ist. """
    if self.typ == 'al': # alle richtigen antworten
      return meine_antworten == self.get_loesungen()
    elif self.typ == 'me': # mindestens eine richtige Antwort
      return meine_antworten.issubset(self.get_loesungen()) and (len(meine_antworten) > 0)


